package com.xl.aspect;

import cn.hutool.core.util.ArrayUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.filter.SimplePropertyPreFilter;
import com.xl.annotation.Log;
import com.xl.entity.LogInfo;
import com.xl.myEnum.BusinessStatus;
import com.xl.utils.IpUtils;
import com.xl.utils.ServletUtils;
import com.xl.utils.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.SourceLocation;
import org.springframework.core.NamedThreadLocal;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Map;

/**
 * 创建切面，切入点是Log注解，对添加log注解的方法进行增强
 */
@Component
@Aspect
public class LogAspect {

    //定义切点 这里等于是吧切点表达式赋值给了pt() 这个方法
    //@Pointcut("@annotation(controllerLog)") // controllerLog指向Log对象
    //public void pt(Log controllerLog){}

    /**
     *公共忽略字段，所有类中的该字段的值都不会被记录进入日志，其他字段的忽略通过Log注解的excludeParamNames属性实现
     */
    public static final String[] INGORE_FIELD = {"password"};

    //线程对象存储请求耗费时间ms
    public static final ThreadLocal<Long> THREAD_LOCAL = new NamedThreadLocal<Long>("cost_time");

    /**
     * 前置增强
     *
     * @param joinPoint     连接点对象，代表被拦截的方法。通过它，你可以访问被拦截方法的详细信息，比如方法签名、目标对象等。
     * @param controllerLog 注解
     */
    @Before("@annotation(controllerLog)")  //直接在这里写切点表达式子，controllerLog 指向Log对象
    //等价写法@Before("pt(controllerLog)") 需要在上面定义执行切点的方法
    public void before(JoinPoint joinPoint, Log controllerLog) {
        THREAD_LOCAL.set(System.currentTimeMillis());
        System.out.println("前置增强");
    }

    /**
     * 后置增强  如果方法执行完之后没有抛出异常（所以异常为null），走这里
     *
     * @param joinPoint     连接点对象，可以通过他拿到被拦截方法的参数和详细信息
     * @param controllerLog
     */
    @AfterReturning(value = "@annotation(controllerLog)", returning = "jsonResult")
    public void handleLogForReturn(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
        System.out.println("后置增强");
        handleLog(joinPoint, controllerLog, null, jsonResult);
        System.out.println("----------------------------");
    }

    /**
     * 异常退出增强 如果执行过程中出现异常（所以returning为null），走这个增强
     *
     * @param joinPoint     连接点 连接点对象，可以通过他拿到被拦截方法的参数和详细信息
     * @param controllerLog
     * @param e             异常对象
     */
    @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e") //e表示，把抛出的异常给到参数e
    public void handleLogForExceptionReturn(JoinPoint joinPoint, Log controllerLog, Exception e) {
        handleLog(joinPoint, controllerLog, e, null);
    }


    /**
     * 环绕增强
     * 使用环绕增强的时候一定要注意：
     *  1 一定要写一个入参 ProceedingJoinPoint，pji.proceed() 之前的代码在被增强方法执行前执行，pji.proceed()之后的代码在被增强方法执行之后执行
     *  2 返回值一定要是一个Object类型，把pji.proceed() 执行的结果返回去，不然被增强方法会一直卡着，没有执行完
     * @param controllerLog
     */
    //@Around(value = "@annotation(controllerLog)")
    //围绕通知需要指定形式参数为一个ProceedingJoinPoint 对象， 在这个对象之前的代码 在前面执行，在这个对象之后的代码在后面执行
    public Object around(ProceedingJoinPoint pji , Log controllerLog) throws Throwable {
        System.out.println("环绕增强,前");
        Object proceed = pji.proceed();
        System.out.println("环绕增强，后");
        System.out.println("---------------------------------");
        return proceed;
    }

    /**
     * @param joinPoint     连接点 通过连接点可以拿到被增强的方法对象，可以拿到方法的参数和
     * @param controllerLog
     * @param e             抛出的异常对象
     */
    private void handleLog(JoinPoint joinPoint, Log controllerLog, Exception e, Object jsonResult) {
        try {
            LogInfo logInfo = new LogInfo();
            //访问时间
            logInfo.setOperTime(new Date(THREAD_LOCAL.get()));
            //获取注解在那个类中
            String className = joinPoint.getTarget().getClass().getName();
            //注解在那个方法上面
            String name = joinPoint.getSignature().getName();
            String requestMethodName = className + "." + name;
            logInfo.setMethod(requestMethodName);

            //获取请求相关信息
            HttpServletRequest request = ServletUtils.getRequest();
            //访问接口的uri
            String requestURI = request.getRequestURI();
            logInfo.setOperUrl(requestURI);
            //请求方式
            String requestMethod = request.getMethod();
            logInfo.setRequestMethod(requestMethod);
            //请求者的ip
            String ipAddr = IpUtils.getIpAddr();
            logInfo.setOperIp(ipAddr);
            //默认是请求成功
            logInfo.setStatus(BusinessStatus.SUCCESS.ordinal());

            //检查是否抛出了异常
            if (StringUtils.isNotNull(e)) {
                //抛出了异常
                logInfo.setStatus(BusinessStatus.FAIL.ordinal());
                logInfo.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
            }

            //处理请求的参数和方法的返回值
            dealWithControllerMethodArgsAndReturn(joinPoint, controllerLog, jsonResult, logInfo);
        } catch (Exception ex) {
            System.out.println("本地异常记录："+ex);
        } finally {
            //及时清理，防止内存泄漏
            THREAD_LOCAL.remove();
        }

    }

    /**
     * 通过Log注解中的参数，判断是否需要对该请求进行记录
     *
     * @param joinPoint
     * @param controllerLog
     * @param jsonResult    可以是抛出的异常信息，也可以是执行完毕之后的返回值
     * @param logInfo
     */
    private void dealWithControllerMethodArgsAndReturn(JoinPoint joinPoint, Log controllerLog, Object jsonResult, LogInfo logInfo) {
        String model = controllerLog.model();//业务类型
        logInfo.setTitle(model);
        int type = controllerLog.operationType().getType();//操作类型
        logInfo.setBusinessType(type);
        //检查是否要记录请求值和返回值
        if (controllerLog.isSaveRequestData()) {
            saveRequestData(joinPoint, controllerLog,logInfo);
        }
        if (controllerLog.isSaveResponseData()) {
            saveResponseData(controllerLog,jsonResult,logInfo);
        }
        logInfo.setCostTime(System.currentTimeMillis()-THREAD_LOCAL.get());
        System.out.println("请求日志记录进入数据库");
        System.out.println(logInfo);

    }

    /**
     * 保存返回值，保存的时候过滤掉 公共过滤字段和
     * @param controllerLog
     * @param jsonResult
     */
    private void saveResponseData(Log controllerLog, Object jsonResult , LogInfo logInfo) {
        String[] excludField = controllerLog.excludeParamNames();
        //将对象转为json字符串，在序列化时排除excludField数组中指定的公共排除字段和注解中指定的排除字段
        SimplePropertyPreFilter simplePropertyPreFilter = new SimplePropertyPreFilter();
        simplePropertyPreFilter.getExcludes().addAll(Arrays.asList(ArrayUtil.addAll(INGORE_FIELD , excludField)));
        String responseResult = JSON.toJSONString(jsonResult, simplePropertyPreFilter);
        logInfo.setJsonResult(responseResult);
    }

    /**
     * 保存请求参数（注解默认是保存请求参数），
     * 如果请求方法是put或者post的时候，通过连接点拿到被增强方法的参数
     * 如果是其他的请求方法，通过httpServletRequest对象拿到请求参数
     * 拿到请求参数后要排除注解里面自定的忽略字段
     *
     * @param joinPoint
     * @param controllerLog
     * @param
     * @param logInfo
     */
    private void saveRequestData(JoinPoint joinPoint, Log controllerLog,  LogInfo logInfo) {
        //通过request对象拿到请求参数
        Map<String, String> paramMap = ServletUtils.getParamMap(ServletUtils.getRequest());
        //通过注解拿到需要忽略的字段
        String[] excludField = controllerLog.excludeParamNames();
        String method = ServletUtils.getRequest().getMethod();
        if (StringUtils.isNotEmpty(method) &&
                ((method.equalsIgnoreCase("PUT")) || (method.equalsIgnoreCase("POST")))) {
            //通过连接点拿到controller方法的入参
            Object[] requestArgs = joinPoint.getArgs();
            //从入参中排除需要忽略的字段并转为String
            String requestResult  = excludArgsToString(requestArgs, excludField);
            logInfo.setOperParam(StringUtils.substring(requestResult, 0, 2000));
        }else {
            //如果是其他的请求方式从请求体中拿到请求参数
            //过滤字段
            SimplePropertyPreFilter simplePropertyPreFilter = new SimplePropertyPreFilter();
            simplePropertyPreFilter.getExcludes().addAll(Arrays.asList(ArrayUtil.addAll(INGORE_FIELD , excludField)));
            logInfo.setOperParam(StringUtils.substring(JSON.toJSONString(paramMap,simplePropertyPreFilter) , 0 ,2000));
        }



    }

    private String excludArgsToString(Object[] requestArgs, String[] excludField) {
        StringBuilder result = new StringBuilder();
        if (StringUtils.isNotEmpty(requestArgs)) {
            //遍历参数列表，排除和文件相关的如参
            for (Object requestArg : requestArgs) {
                //如果这个参数不是空，且不包含文件，那就不过滤这个入参
                if (StringUtils.isNotNull(requestArg) && !isFilterField(requestArg)){
                    //将对象转为json字符串，在序列化时排除excludField数组中指定的公共排除字段和注解中指定的排除字段
                    SimplePropertyPreFilter simplePropertyPreFilter = new SimplePropertyPreFilter();
                    simplePropertyPreFilter.getExcludes().addAll(Arrays.asList(ArrayUtil.addAll(INGORE_FIELD , excludField)));
                    result.append(JSON.toJSONString(requestArg, simplePropertyPreFilter)).append("  ");
                }
            }
        }
        return result.toString();
    }


    /**
     * 检查某个对象是否要被过滤掉
     *
     * @param obj
     * @return 过滤true 不过滤false
     * 这里涉及到两个方法 instanceOf 和 isAssignableFrom
     * A instanceOf B ，*用于对象实例与类（或接口之间）*：判断A是否是B这个类（或接口）的实例，或者A是否是B类（或接口）的子类或者实例。
     * A.class.isAssignableFrom B.class *用于class和class之间*：判断A类是否是B类是否存在继承或者实现关系。
     * 如果obj是数组，且数组中的元素是文件类型，直接过滤掉这个数组
     * 如果obj是集合，循环判断集合中的对象是否是文件，如果是，过滤掉这个元素
     * 如果obj是map，且map中的值是文件，过滤掉这个值
     */
    private boolean isFilterField(Object obj) {
        Class clazz = obj.getClass();
        if (clazz.isArray()) {
            //clazz.getComponentType() 获取数组元素的Class对象
            //Class对象.isAssignableFrom(cls) 用于判断cls对象是否是Class对象的子类或者实现
            //判断的过过程中，如果是判断一个类cls是否是class的实现，使用他们的class对象，进行isAssignableFrom(cls)比较
            //判断链各个
            //如果对象是数组，并且数组中的元素是MultipartFile的数据；如果数组里面存放的是文件数据就过滤掉,不是文件数据就不过滤
            if (clazz.getComponentType().isAssignableFrom(MultipartFile.class)) {
                return true;
            }
            return false;
        } else if (Controller.class.isAssignableFrom(clazz)) {
            //如果obj是一个集合类型
            Collection collection = (Collection) obj;
            for (Object o : collection) {
                //如果数组中的元素是文件类型的不过滤，其他不过滤
                return o instanceof MultipartFile;
            }
        } else if (Map.class.isAssignableFrom(clazz)) {
            //如果对象是一个map集合，
            Map map = (Map) obj;
            for (Object o : map.entrySet()) {
                Map.Entry entry = (Map.Entry) o;
                //如果map集合中的元素的类型是文件类型，就过滤，其他不过滤
                return entry.getValue() instanceof MultipartFile;
            }
        }
        //如果obj是文件、HttpServletRequest、HttpServletResponse、BindingResult 对象的实例都过滤掉。其他的都不过滤
        return obj instanceof MultipartFile || obj instanceof HttpServletRequest || obj instanceof HttpServletResponse
                || obj instanceof BindingResult;
    }


}
